package org.ccay.validator.impl;

//import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.Map.Entry;

import org.springframework.context.ApplicationContext;

import org.ccay.core.base.BaseResourceVO;
import org.ccay.core.log.ILogger;
import org.ccay.core.log.CcayLoggerFactory;
import org.ccay.core.util.StreamUtil;
import org.ccay.validator.ActionValidationMessage;
import org.ccay.validator.FieldValidationMessage;
import org.ccay.validator.IValidatorContext;
import org.ccay.validator.IValidatorContextAware;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ObjectFactory;
import com.opensymphony.xwork2.config.Configuration;
import com.opensymphony.xwork2.config.ConfigurationManager;
import com.opensymphony.xwork2.config.providers.XmlConfigurationProvider;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.spring.SpringObjectFactory;
import com.opensymphony.xwork2.util.FileManager;
import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.validator.ActionValidatorManager;
import com.opensymphony.xwork2.validator.DefaultActionValidatorManager;
import com.opensymphony.xwork2.validator.FieldValidator;
import com.opensymphony.xwork2.validator.ShortCircuitableValidator;
import com.opensymphony.xwork2.validator.ValidationException;
import com.opensymphony.xwork2.validator.Validator;
import com.opensymphony.xwork2.validator.ValidatorConfig;
import com.opensymphony.xwork2.validator.ValidatorContext;
import com.opensymphony.xwork2.validator.ValidatorFactory;
import com.opensymphony.xwork2.validator.ValidatorFileParser;
import com.opensymphony.xwork2.validator.validators.VisitorFieldValidator;

/**
 * This is the entry point into XWork's rule-based validation framework.
 * <p/>
 * Validation rules are specified in XML configuration files named <code>className-contextName-validation.xml</code> where
 * className is the name of the class the configuration is for and -contextName is optional
 * (contextName is an arbitrary key that is used to look up additional validation rules for a
 * specific context).
 *
 * @author chaowangbang
 * 
 */
@SuppressWarnings("rawtypes")
public class DefaultValidatorManager extends DefaultActionValidatorManager {
	
	/** 日志类 */
	private final ILogger logger = CcayLoggerFactory.getLogger(DefaultValidatorManager.class);
    
    /** The file suffix for any validation file. */
    protected static final String VALIDATION_CONFIG_SUFFIX = "-validation.xml";
    
    protected final Map<String, List<ValidatorConfig>> validatorCache = new HashMap<String, List<ValidatorConfig>>();
    protected final Map<String, List<ValidatorConfig>> validatorFileCache = new HashMap<String, List<ValidatorConfig>>();
    protected ValidatorFactory validatorFactory;
    protected ValidatorFileParser validatorFileParser;
    
    public static ActionValidatorManager createInstance(ApplicationContext ctx) {
    	// 默认查询validator-default.xml里的配置信息
		ConfigurationManager configurationManager = new ConfigurationManager("default");
		configurationManager.addContainerProvider(new XmlConfigurationProvider( "validator-default.xml", false));

		Configuration config = configurationManager.getConfiguration();
		Container container = config.getContainer();
		
		// 默认的工厂类没有装配上下文，在这里装配
		SpringObjectFactory objectFactory = (SpringObjectFactory)container.getInstance(ObjectFactory.class);
		objectFactory.setApplicationContext(ctx);

		// 从验证容器中取出验证管理器
		ActionValidatorManager actionValidatorManager = container.getInstance(ActionValidatorManager.class);
		return actionValidatorManager;
    }

    @Inject
    public void setValidatorFileParser(ValidatorFileParser parser) {
        this.validatorFileParser = parser;
        super.setValidatorFileParser(parser);
    }
    
    @Inject
    public void setValidatorFactory(ValidatorFactory fac) {
        this.validatorFactory = fac;
        super.setValidatorFactory(fac);
    }
    
    /**
     * @param clazz
     * @param validatorContext
     * @param method
     * @return
     */
    public List<Validator> getValidators(Object serviceClazz, IValidatorContext validatorContext, String method) {
		// 缓存key
		final String validatorKey = buildValidatorsKey(serviceClazz.getClass(), validatorContext, method);
		logger.debug("validatorKey=" +validatorKey );
        List<ValidatorConfig> cfgs = validatorCache.get(validatorKey);
		
		if(cfgs == null) {
			cfgs = buildValidatorConfigs(serviceClazz.getClass(), false, null, validatorKey);
			validatorCache.put(validatorKey, cfgs);
        }
        ValueStack stack = ActionContext.getContext().getValueStack();

        // create clean instances of the validators for the caller's use
        List<Validator> validators = new ArrayList<Validator>(cfgs.size());
        for (ValidatorConfig cfg : cfgs) {
        	Validator validator = validatorFactory.getValidator(cfg);
    	    validator.setValidatorType(cfg.getType());
    	    validator.setValueStack(stack);
    	    validators.add(validator);
        }
        return validators;
    }
    
    /**
     * Builds a key for validators - used when caching validators.
     *
     * @param clazz the action.
     * @param context the action's context.
     * @return a validator key which is the class name plus context.
     */
    protected String buildValidatorsKey(Class clazz, IValidatorContext context, String method) {
        StringBuilder sb = new StringBuilder(clazz.getName());
        sb.append("/");
        
        // 获取方法索引
        String argIndex = context.getParam("argIndex", String.class);
        String validatorKeyPostfix = "." + method + "[" + argIndex + "]";
        // 获取解析节点名称
        String tagName = context.getParam("tagName", String.class);
        
		if (null != tagName) {
			validatorKeyPostfix = validatorKeyPostfix + "." + tagName;
		}
		//获取节点下 使用的对象名称  区分 新增和编辑 使用的不同对象验证 
        String elementName = context.getParam("elementName", String.class);
		if (null != elementName) {
			validatorKeyPostfix = validatorKeyPostfix + "." + elementName;
		}
        sb.append(validatorKeyPostfix);
        
        return sb.toString();
    }

    /* （非 Javadoc）
     * @see com.opensymphony.xwork2.validator.DefaultActionValidatorManager#validate(java.lang.Object, java.lang.String, com.opensymphony.xwork2.validator.ValidatorContext, java.lang.String)
     */
    public void validate(Object serviceClazz, String context, ValidatorContext validatorContext, String method) throws ValidationException {
		if (!(validatorContext instanceof IValidatorContext)) {
			super.validate(serviceClazz, context, validatorContext, method);
			return;
		}
		
		IValidatorContext ctx = (IValidatorContext) validatorContext;
		
		// 设置解析器的上下文k
		if (validatorFileParser instanceof IValidatorContextAware) {
			IValidatorContextAware aware = (IValidatorContextAware) validatorFileParser;
			aware.setValidatorContext(ctx);
		}
    
    	List<Validator> validators = getValidators(serviceClazz, ctx, method);

        Set<String> shortcircuitedFields = null;
        
        Object object = ctx.getParam("validatorTarget", Object.class);

        for (final Validator validator : validators) {
        	try {
                validator.setValidatorContext(validatorContext);

                logger.debug("Running validator: " + validator + " for object " + object + " and method " + method);

                FieldValidator fValidator = null;
                String fullFieldName = null;

                if (validator instanceof FieldValidator) {
                    fValidator = (FieldValidator) validator;
                    fullFieldName = new InternalValidatorContextWrapper(fValidator.getValidatorContext()).getFullFieldName(fValidator.getFieldName());

                    if ((shortcircuitedFields != null) && shortcircuitedFields.contains(fullFieldName)) {
                    	logger.debug("Short-circuited, skipping");
                        continue;
                    }
                }

                if (validator instanceof ShortCircuitableValidator && ((ShortCircuitableValidator) validator).isShortCircuit()) {
                    // get number of existing errors
                    List<String> errs = null;

                    if (fValidator != null) {
                        if (validatorContext.hasFieldErrors()) {
                            Collection<String> fieldErrors = validatorContext.getFieldErrors().get(fullFieldName);

                            if (fieldErrors != null) {
                                errs = new ArrayList<String>(fieldErrors);
                            }
                        }
                    } else if (validatorContext.hasActionErrors()) {
                        Collection<String> actionErrors = validatorContext.getActionErrors();

                        if (actionErrors != null) {
                            errs = new ArrayList<String>(actionErrors);
                        }
                    }

                    validator.validate(object);

                    if (fValidator != null) {
                        if (validatorContext.hasFieldErrors()) {
                            Collection<String> errCol = validatorContext.getFieldErrors().get(fullFieldName);

                            if ((errCol != null) && !errCol.equals(errs)) {
                            	logger.debug("Short-circuiting on field validation");

                                if (shortcircuitedFields == null) {
                                    shortcircuitedFields = new TreeSet<String>();
                                }

                                shortcircuitedFields.add(fullFieldName);
                            }
                        }
                    } else if (validatorContext.hasActionErrors()) {
                        Collection<String> errCol = validatorContext.getActionErrors();

                        if ((errCol != null) && !errCol.equals(errs)) {
                        	logger.debug("Short-circuiting");
                            break;
                        }
                    }

                    continue;
                }

                validator.validate(object);
            }
            finally {
                validator.setValidatorContext(null);
            }
        }
        
        if(!(object instanceof Collection) && !(object instanceof Object[])) {
        	bindingRowIndex2Message(object, ctx, validatorContext);
        }
    }
 
    private void bindingRowIndex2Message(Object bean, IValidatorContext ctx, ValidatorContext xworkCtx) {
    	Collection<String> actionErrors = xworkCtx.getActionErrors();
    	Map<String, List<String>> fieldErrors = xworkCtx.getFieldErrors();
    	
    	if(actionErrors.isEmpty() && fieldErrors.isEmpty()) {
    		return;
    	}
    	
    	int rowIndex = -1;
    	if(bean instanceof BaseResourceVO) {
    		rowIndex = ((BaseResourceVO)bean).getRowIdx();
    	}
    	    	
    	for(Iterator<String> iter = actionErrors.iterator(); iter.hasNext(); ) {
    		ActionValidationMessage message = new ActionValidationMessage();
    		message.setRowIndex(rowIndex);
    		message.setMessage(iter.next());
    		ctx.addActionError(message);
    	}
    	actionErrors.clear();
    	xworkCtx.setActionMessages(new ArrayList<String>());
    	for(Iterator<Entry<String, List<String>>> iter = fieldErrors.entrySet().iterator(); iter.hasNext(); ) {
    		Entry<String, List<String>> entry = iter.next();
    		String fieldName = entry.getKey();
    		List<String> errors = entry.getValue();
    		
    		FieldValidationMessage message = new FieldValidationMessage();
    		message.setRowIndex(rowIndex);
    		message.setMessages(errors);
    		message.setFieldName(fieldName);
    		
    		ctx.addFieldError(message);
    	}
    	fieldErrors.clear();
    	xworkCtx.setFieldErrors(new HashMap<String,List<String>>());
    }

    /**
     * <p>This method 'collects' all the validator configurations for a given
     * action invocation.</p>
     *
     * <p>It will traverse up the class hierarchy looking for validators for every super class
     * and directly implemented interface of the current action, as well as adding validators for
     * any alias of this invocation. Nifty!</p>
     *
     * <p>Given the following class structure:
     * <pre>
     *   interface Thing;
     *   interface Animal extends Thing;
     *   interface Quadraped extends Animal;
     *   class AnimalImpl implements Animal;
     *   class QuadrapedImpl extends AnimalImpl implements Quadraped;
     *   class Dog extends QuadrapedImpl;
     * </pre></p>
     *
     * <p>This method will look for the following config files for Dog:
     * <pre>
     *   Animal
     *   Animal-context
     *   AnimalImpl
     *   AnimalImpl-context
     *   Quadraped
     *   Quadraped-context
     *   QuadrapedImpl
     *   QuadrapedImpl-context
     *   Dog
     *   Dog-context
     * </pre></p>
     *
     * <p>Note that the validation rules for Thing is never looked for because no class in the
     * hierarchy directly implements Thing.</p>
     *
     * @param clazz the Class to look up validators for.
     * @param context the context to use when looking up validators.
     * @param checkFile true if the validation config file should be checked to see if it has been
     *      updated.
     * @param checked the set of previously checked class-contexts, null if none have been checked
     * @return a list of validator configs for the given class and context.
     */
    private List<ValidatorConfig> buildValidatorConfigs(Class clazz, boolean checkFile, Set<String> checked, String validatorKey) {
        List<ValidatorConfig> validatorConfigs = new ArrayList<ValidatorConfig>();

        if (checked == null) {
            checked = new TreeSet<String>();
        } else if (checked.contains(clazz.getName())) {
            return validatorConfigs;
        }

        processInterface(clazz, checkFile, checked, validatorConfigs, validatorKey);

        // look for validators for implemented interfaces
        lookInterfaces(clazz, checkFile, checked, validatorConfigs, validatorKey);

        validatorConfigs.addAll(buildClassValidatorConfigs(clazz, checkFile, validatorKey));

        checked.add(clazz.getName());

        return validatorConfigs;
    }

	/**
	 * @param clazz
	 * @param context
	 * @param checkFile
	 * @param checked
	 * @param validatorConfigs
	 */
	private void processInterface(Class clazz, boolean checkFile, Set<String> checked, List<ValidatorConfig> validatorConfigs, String validatorKey) {
		if (clazz.isInterface()) {
            for (Class anInterface : clazz.getInterfaces()) {
                validatorConfigs.addAll(buildValidatorConfigs(anInterface, checkFile, checked, validatorKey));
             }
        } else {
            if (!clazz.equals(Object.class)) {
                validatorConfigs.addAll(buildValidatorConfigs(clazz.getSuperclass(), checkFile, checked, validatorKey));
            }
        }
	}

	/**
	 * look for validators for implemented interfaces
	 * 
	 * @param clazz
	 * @param context
	 * @param checkFile
	 * @param checked
	 * @param validatorConfigs
	 */
	private void lookInterfaces(Class clazz, boolean checkFile, Set<String> checked, List<ValidatorConfig> validatorConfigs, String validatorKey) {
		for (Class anInterface1 : clazz.getInterfaces()) {
            if (checked.contains(anInterface1.getName())) {
                continue;
            }

            validatorConfigs.addAll(buildClassValidatorConfigs(anInterface1, checkFile, validatorKey));

            checked.add(anInterface1.getName());
        }
	}

    /**
     * @param aClass
     * @param checkFile
     * @return
     */
    private List<ValidatorConfig> buildClassValidatorConfigs(Class aClass, boolean checkFile, String validatorKey) {
        String fileName = aClass.getName().replace('.', '/') + VALIDATION_CONFIG_SUFFIX;

        return loadFile(fileName, aClass, checkFile, validatorKey);
    }

    /**
     * @param fileName
     * @param clazz
     * @param checkFile
     * @return
     */
    private List<ValidatorConfig> loadFile(String fileName, Class clazz, boolean checkFile, String validatorKey) {
        List<ValidatorConfig> retList = Collections.emptyList();
        String cacheKey = fileName;
        if (validatorKey.startsWith(clazz.getName())) {
        	cacheKey = validatorKey;
        }        
        logger.debug("cacheKey="+cacheKey);
        if(checkFile){
        	retList = readFromFile(fileName, clazz, retList);
            validatorFileCache.put(cacheKey, retList);
            return retList;
        }
        
        List<ValidatorConfig> listInCache = validatorFileCache.get(cacheKey);
        
        if(listInCache == null){
        	retList = readFromFile(fileName, clazz, retList);
            validatorFileCache.put(cacheKey, retList);
            return retList;
        }
        
        return listInCache;         
    }

	/**
	 * 从文件中读取配置
	 * 
	 * @param fileName
	 * @param clazz
	 * @param retList
	 * @return
	 */
	private List<ValidatorConfig> readFromFile(String fileName, Class clazz, List<ValidatorConfig> retList) {
		InputStream is = null;
		try {
		    is = FileManager.loadFile(fileName, clazz);

		    if (is != null) {
		    	List<ValidatorConfig> validatorConfigs = validatorFileParser.parseActionValidatorConfigs(validatorFactory, is, fileName);
		        retList = new ArrayList<ValidatorConfig>(validatorConfigs);
		    }
		} finally {
		    StreamUtil.closeStreams(is);
		}
		return retList;
	}


    /**
     * An {@link com.opensymphony.xwork2.validator.ValidatorContext} wrapper that
     * returns the full field name
     * {@link InternalValidatorContextWrapper#getFullFieldName(String)}
     * by consulting it's parent if its an {@link com.opensymphony.xwork2.validator.validators.VisitorFieldValidator.AppendingValidatorContext}.
     * <p/>
     * Eg. if we have nested Visitor
     * AddressVisitor nested inside PersonVisitor, when using the normal #getFullFieldName, we will get
     * "address.somefield", we lost the parent, with this wrapper, we will get "person.address.somefield".
     * This is so that the key is used to register errors, so that we don't screw up short-curcuit feature
     * when using nested visitor. See XW-571 (nested visitor validators break short-circuit functionality)
     * at http://jira.opensymphony.com/browse/XW-571
     */
    protected class InternalValidatorContextWrapper {
        private ValidatorContext validatorContext = null;

        InternalValidatorContextWrapper(ValidatorContext validatorContext) {
            this.validatorContext = validatorContext;
        }

        /**
         * Get the full field name by consulting the parent, so that when we are using nested visitors (
         * visitor nested inside visitor etc.) we still get the full field name including its parents.
         * See XW-571 for more details.
         * @param field The field name
         * @return String
         */
        public String getFullFieldName(String field) {
            if (validatorContext instanceof VisitorFieldValidator.AppendingValidatorContext) {
                VisitorFieldValidator.AppendingValidatorContext appendingValidatorContext =
                        (VisitorFieldValidator.AppendingValidatorContext) validatorContext;
                return appendingValidatorContext.getFullFieldNameFromParent(field);
            }
            return validatorContext.getFullFieldName(field);
        }

    }

}
